/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.alink.common.io.catalog.datahub.common.errorcode;

import org.apache.commons.lang3.StringUtils;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.text.DateFormat;
import java.text.Format;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * define static error code instances and implements logic
 *
 * <p>it serves as:
 * I.   Define static error code instances per module:
 * table/sql api, blink sql parser, runtime, state backend, and connector
 * II.  Define some annotations for use.
 * III. Implement logic for proxy instance creating, error message assembly, and validation.
 */
public class ErrorFactory {

	protected static Pattern pattern1 = Pattern.compile("[0-9]{8}");
	protected static Set<String> modNames = new HashSet<>();

	static {
		modNames.add("SQL");
		modNames.add("PAR");
		modNames.add("CON");
		modNames.add("STB");
		modNames.add("RUN");
	}

	private ErrorFactory() {
	}

	/**
	 * create proxy instance for one module's error code interface.
	 *
	 * @param clazz interface that has error code definitions for one module.
	 * @return instance of the interface that can be used by developer for specifying error code
	 * (for now, it is for dumping error code and its cause&action message)
	 * when throwing exceptions
	 */
	public static Object createProxy(Class clazz) {

		return Proxy.newProxyInstance(
			clazz.getClassLoader(),
			clazz.isInterface() ? new Class[]{clazz} : clazz.getInterfaces(),
			(obj, method, args) -> {
				checkParam(method, args);
				return assemblyErrCodeString(method, args);
			});

	}

	/**
	 * Parameter check when invoking method.
	 *
	 * @param method
	 * @param args
	 */
	protected static void checkParam(Method method, Object[] args) {
		ErrCode errCode = method.getAnnotation(ErrCode.class);
		String errDetail = errCode.details();
		String errCause = errCode.cause();

		MessageFormat format1 = new MessageFormat(errDetail);
		MessageFormat format2 = new MessageFormat(errCause);

		if (args == null || args.length == 0) {
			if ((format1.getFormatsByArgumentIndex() != null && format1.getFormatsByArgumentIndex().length > 0)
				|| (format2.getFormatsByArgumentIndex() != null && format2.getFormatsByArgumentIndex().length > 0)) {
				throw new AssertionError("mismatched parameter length between "
					+ method.getName() + " and its annotation @ErrCode");
			}
		} else {
			if ((format1.getFormatsByArgumentIndex() != null && format1.getFormatsByArgumentIndex().length > args.length)
				|| format1.getFormatsByArgumentIndex() == null
				|| (format2.getFormatsByArgumentIndex() != null && format2.getFormatsByArgumentIndex().length > args.length)) {
				throw new AssertionError("mismatched parameter length between "
					+ method.getName() + " and its annotation @ErrCode");
			}
		}
	}

	/**
	 * assembly error code messages.
	 *
	 * @param method error code related function declared in error interface
	 * @param args   args passed to that related function
	 * @return error code messages containing code id, cause and action.
	 */
	protected static String assemblyErrCodeString(Method method, Object[] args) {
		ErrCode errCode = method.getAnnotation(ErrCode.class);
		String errId = errCode.codeId();
		String errCause = errCode.cause();
		String errDetail = errCode.details();
		String errAction = errCode.action();

		if (args != null && args.length != 0) {
			MessageFormat format1 = new MessageFormat(errDetail);
			errDetail = format1.format(args);

			MessageFormat format2 = new MessageFormat(errCause);
			errCause = format2.format(args);
		}

		errId = prettyPrint(errId);
		errCause = prettyPrint(errCause);
		errDetail = prettyPrint(errDetail);
		errAction = prettyPrint(errAction);

		String msg = "\n************\n"
			//"\n*******************************************************\n"
			+ "ERR_ID:\n"
			+ errId + "\n"
			+ "CAUSE:\n"
			+ errCause + "\n"
			+ "ACTION:\n"
			+ errAction + "\n"
			+ "DETAIL:\n"
			+ errDetail + "\n"
			//+ "*******************************************************";
			+ "************";
		return msg;
	}

	/**
	 * Print out error code in a pretty way.
	 *
	 * @param str
	 * @return
	 */
	public static String prettyPrint(String str) {
		if (str != null && str.length() != 0) {
			str = indent(5) + str.replaceAll("\n", "\n" + indent(5));
		}

		return str;
	}

	/**
	 * validate an error code definition interface to check its annotation usage
	 * and err code format.
	 *
	 * @param clazz err code definition interface
	 */
	public static void validate(Class<?> clazz) {
		validate(clazz, EnumSet.allOf(ValidationType.class));
	}

	/**
	 * validate an error code definition interface to check its annotation usage
	 * and err code format.
	 *
	 * @param clazz       err code definition interface
	 * @param validations types of validations to perform.
	 */
	public static void validate(Class<?> clazz, EnumSet<ValidationType> validations) {
		int cnt = 0;
		Set<String> errIds = new HashSet<>();

		for (Method method : clazz.getMethods()) {
			if (!Modifier.isStatic(method.getModifiers())) {
				cnt++;

				final ErrCode anno1 = method.getAnnotation(ErrCode.class);

				for (ValidationType validation : validations) {
					switch (validation) {
						case ANNOTATION_SPECIFIED:
							if (anno1 == null || StringUtils.isEmpty(anno1.codeId())
								|| StringUtils.isEmpty(anno1.cause())) {
								throw new AssertionError(String.format("error code method[%s] "
										+ "must specify @ErrCode annotation with cause, details and none-empty codeId!",
									method.getName()));
							}
							break;

						case ERROR_ID_CHECK:
							if (anno1 == null) {
								throw new AssertionError(String.format("error code method[%s]"
										+ "has no @ErrId annotation!",
									method.getName()));
							}
							String errId = anno1.codeId();
							if (!checkErrorCodeFmt(errId)) {
								throw new AssertionError(String.format("error code method[%s]"
										+ "has invalid error code: %s",
									method.getName(),
									errId));
							}
							if (errIds.contains(errId)) {
								throw new AssertionError(String.format("error code method[%s]"
										+ "has duplicated err id: %s",
									method.getName(),
									errId));
							}
							errIds.add(errId);
							break;

						case ARGUMENT_MATCH:
							if (anno1 == null) {
								throw new AssertionError(String.format("error code method[%s]"
										+ "has no @ErrCode annotation!",
									method.getName()));
							}

							String msg = anno1.details();
							String cause = anno1.cause();

							MessageFormat msgFmt = new MessageFormat(msg);
							MessageFormat causeFmt = new MessageFormat(cause);

							final Format[] msgFormats = msgFmt.getFormatsByArgumentIndex();
							final Format[] causeFormats = causeFmt.getFormatsByArgumentIndex();

							final Format[] formats = msgFormats.length != 0 ? msgFormats : causeFormats;

							final List<Class> types = new ArrayList<>();
							final Class<?>[] paramTypes = method.getParameterTypes();

							/* implement check on case when only one field get parameter(s)
							 * TODO implement check on case when both fields get parameter(s)
							 */
							if (!(msgFormats.length != 0 && causeFormats.length != 0)) {

								for (int i = 0; i < formats.length; i++) {
									Format fmt1 = formats[i];
									Class paramType = paramTypes[i];
									final Class<?> e;
									if (fmt1 instanceof NumberFormat) {
										e = paramType == short.class
											|| paramType == int.class
											|| paramType == long.class
											|| paramType == float.class
											|| paramType == double.class
											|| Number.class.isAssignableFrom(paramType)
											? paramType
											: Number.class;
									} else if (fmt1 instanceof DateFormat) {
										e = Date.class;
									} else {
										e = String.class;
									}

									types.add(e);
								}

								final List<Class<?>> paramTypeList = Arrays.asList(paramTypes);
								if (!types.equals(paramTypeList)) {
									throw new AssertionError(String.format("error code[%s]"
											+ " has type mismatch(s) between method param %s and"
											+ " format elements %s in annotation",
										method.getName(),
										types,
										paramTypeList
									));
								}
							}
							break;

						default:
							break;
					}
				}
			}
		}

		if (cnt == 0 && validations.contains(ValidationType.AT_LEAST_ONE)) {
			throw new AssertionError(clazz + " contains no error code");
		}
	}

	/**
	 * Check error code id format.
	 *
	 * @param errId
	 * @return
	 */
	protected static boolean checkErrorCodeFmt(String errId) {
		if (errId == null || errId.isEmpty()) {
			return false;
		}
		String[] parts = errId.split("-");
		if (parts.length != 2) {
			return false;
		}
		if (!modNames.contains(parts[0])) {
			return false;
		}
		if (!pattern1.matcher(parts[1]).matches()) {
			return false;
		}

		return true;
	}

	/**
	 * Get multiple indent.
	 *
	 * @param cnt
	 * @return
	 */
	public static String indent(int cnt) {
		if (cnt <= 0) {
			return null;
		}
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < cnt; i++) {
			sb.append(" ");
		}
		return sb.toString();
	}

	/**
	 * Types of validation that can be performed on a resource.
	 */
	public enum ValidationType {
		/**
		 * Checks that the ErrId, ErrCause and ErrAction annotations are on every resource.
		 */
		ANNOTATION_SPECIFIED,

		/**
		 * Checks that there is at least one resource.
		 */
		AT_LEAST_ONE,

		/**
		 * Checks that @ErrId anno has non-null value which has expected error id format,
		 * and there's no duplicated error id in resource instance.
		 */
		ERROR_ID_CHECK,

		/**
		 * Checks that the parameters of the method are consistent with the
		 * format elements in the error cause message.
		 */
		ARGUMENT_MATCH,
	}

	/**
	 * err code id, cause, detailed message and action.
	 **/
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.METHOD)
	public @interface ErrCode {
		String codeId();

		String cause();

		String details();

		String action();
	}

}
